18. Praktyka Reacta

Wyzwania:

  • nauczysz się wykorzystywać React do obsługi formularzy,
  • zdobędziesz doświadczenie w zakresie korzystania z Reduksa,
  • rozbudujesz aplikację agencji turystycznej.

18.1. Założenia projektu

W poprzednim module musieliśmy szybko wskoczyć na główkę do nowego projektu, błyskawicznie zorientować się w jego strukturze i dopisać kilka nowych funkcjonalności. Nieco się przy tym napociliśmy, ale ostatecznie wszystko się udało, a dział obsługi z radością poinformował nas, że klient (oraz jego prawnik) jest pod wrażeniem tempa i jakości naszej pracy. W związku z tym klient zdecydował się zlecić nam dalszą rozbudowę jego serwisu!

W tym module polecenia będą mniej szczegółowe niż poprzednio – będziesz pracować bardziej samodzielnie, żeby jak najszybciej przyzwyczaić się do projektowania struktury aplikacji i planowania jej działania. Zacznijmy jednak od przyjrzenia się nowej liście zadań od naszego klienta...

Zarys działania serwisu

Aplikacja, nad którą pracujemy, to strona bardzo luksusowego biura podróży. Oferuje ono wyszukane wycieczki z opcjami dla najbardziej wymagających klientów, a przy tym nie przewiduje żadnych zniżek ani np. ofert last minute. Wszystko na najwyższym poziomie i dopięte na ostatni guzik!

Przelot prywatnym odrzutowcem do Australii na fotograficzne safari w towarzystwie fotografa z National Geographic, zakwaterowanie w luksusowej willi z pełną obsługą, w wolnych chwilach nurkowanie na rafie koralowej i wycieczki jachtem, a do tego jeszcze opieka i atrakcje dla dzieci? Żaden problem... za odpowiednią cenę.

Ponieważ oferowane wycieczki są tak luksusowe i dostosowane do konkretnego klienta, na stronie nie zobaczymy sztywnego terminarza wyjazdów ani formularza płatności. Gdy użytkownik wybierze wycieczkę i jej opcje, skontaktuje się z nim pracownik biura, aby osobiście dograć wszystkie szczegóły.

Składanie zamówienia i polityka cenowa

W związku z powyższym nasz klient na stronie swojego biura potrzebuje tak naprawdę rozbudowanego formularza kontaktowego, widocznego na stronie każdej wycieczki. Użytkownicy będą mieli do wyboru kilka opcji personalizacji swojej podróży, ale powinniśmy przewidzieć architekturę aplikacji w taki sposób, aby w przyszłości można było łatwo dodawać kolejne opcje.

Każda opcja ma mieć wpływ na ostateczną cenę wyprawy. Cena poszczególnych opcji będzie albo konkretną kwotą w dolarach, albo mnożnikiem procentowym (np. +20% do ceny). Finalna cena wycieczki to jej cena bazowa (pobrana z pliku trips.json) powiększona o opcje z ceną w dolarach, a następnie przemnożona przez sumę mnożników ceny (np. za każdą osobę dorosłą doliczamy 100% ceny bazowej z opcjami z ceną kwotową).

Przechowywanie danych

W celu podniesienia jakości usług strona biura powinna działać tak szybko, jak to możliwe. Z tego powodu cały cennik będzie przechowywany jako json w plikach aplikacji, a nie pobierany z API – możemy tak zrobić, bo ceny w biurze będą się rzadko zmieniać.

W stanie aplikacji będziemy przechowywać tylko informacje o tym, jakie opcje zostały wybrane – nie potrzebujemy przechowywać w stanie cen opcji ani ceny całkowitej za wycieczkę, gdyż będzie ona obliczana w momencie, kiedy będzie taka potrzeba.

Uff! Sporo zadań! Niezrażeni tym jednak zakasujemy rękawy i zabieramy się do pracy. Na początek przyjrzymy się nowym funkcjom i komponentom, które już za chwilę dodamy do projektu.

18.2. Przygotowanie do rozbudowy projektu

Naszym celem w tym submodule jest zdobycie większej wprawy w korzystaniu z formularzy w projekcie reactowym oraz powiązaniem ich z reduksowym magazynem. Dlatego przygotowaliśmy dla Ciebie kilka plików, które pozwolą nam szybciej przejść do tych zagadnień.

Pobierz paczkę plików

Poniżej znajdziesz informacje na temat każdego z plików, a także instrukcje niezbędne do umieszczenia ich w projekcie.

Cennik opcji

W pliku pricing.json znajdziesz konfigurację wszystkich opcji naszych wycieczek. Każda z opcji posiada też swoją cenę, która będzie doliczana do ceny wycieczki.

Jak wspomnieliśmy wcześniej, zależy nam na tym, aby opcje mogły być łatwo dodawane i zmieniane. Dlatego założyliśmy, że wystarczą nam cztery typy opcji zamówienia:

  1. dropdown, czyli HTML-owy <select>, pozwalający na wybranie jednej wartości,
  2. icons, które mają pozwalać na wybranie jednej wartości poprzez kliknięcie "guzika" zawierającego ikonę,
  3. checkboxes, czyli opcja pozwalająca na wybranie kilku wartości,
  4. number, czyli wybór liczby – np. ilości osób biorących udział w wycieczce.

Ten plik będziemy wykorzystywać do wyświetlenia opcji w naszym formularzu zamówienia. Umieść go w katalogu src/data.

Selektory, akcje i reducer

Plik orderRedux.js nie powinien być dla Ciebie nowością – pisaliśmy już podobne pliki kilkukrotnie. Znajdziesz w nim:

  • selektory pozwalające na odczytanie całego zamówienia lub tylko jego opcji,
  • akcję SET_OPTION, która będzie służyła do aktualizacji wartości danej opcji w stanie aplikacji,
  • reducer reagujący na akcję SET_OPTION.

W reducerze zwróć szczególną uwagę na sposób aktualizacji statePart poprzez rozpakowanie zarówno samego statePart, jak i statePart.options. Pamiętaj, że reducer nie może zmieniać statePart, ani żadnego obiektu (ani tablicy) pobranych z niego. Dlatego tworzymy nowe obiekty, rozpakowując do nich wartości otrzymane w statePart.

Jeśli nie jest to dla Ciebie jasne, wróć do analizy tego reducera po wprowadzeniu zmian w pliku store.js (poniżej).

Umieść plik orderRedux.js w katalogu src/redux.

Funkcje pomocnicze

W katalogu utils znajdziesz kilka funkcji pomocniczych.

  1. parseTrips.js to nowa wersja pliku, która zawiera kod odpowiedzialny za wygenerowanie pierwotnego obiektu order w stanie aplikacji, w oparciu o definicję opcji zamówienia znajdującą się w pricing.json.
  2. formatPrice.js zawiera funkcję, która formatuje otrzymaną w argumencie liczbę do odpowiedniego formatu – w tym przypadku, zaokrągla liczbę oraz stosuje amerykańską notację liczb (przecinki jako separatory tysięcy) i dodaje symbol waluty ($). Jeśli do funkcji formatPrice przekazano nie-liczbę, czyli np. tekst, to taki argument zostanie zwrócony bez wprowadzania zmian.
  1. parseOptionPrice.js dostarcza nam funkcję, która rozpozna, czy otrzymała liczbę, czy tekst ze sformatowaną ceną, czy też współczynnik procentowy (np. 50%) – dzięki temu będzie nam łatwiej rozpoznawać ceny zapisane w pricing.json.
  2. calculateTotal.js zawiera dość skomplikowaną funkcję, która oblicza cenę zamówienia na podstawie bazowej ceny wycieczki, opcji zapisanych w stanie aplikacji, oraz danych z pliku pricing.json. Ta funkcja nie jest skomplikowana od strony czysto programistycznej – jest za to wymagająca algorytmicznie. Dlatego przygotowaliśmy ją dla Ciebie – postaraj się jednak wrócić do tego pliku po zakończeniu niniejszego projektu, aby przeanalizować jej działanie.

Umieść te pliki w katalogu src/utils, nadpisując dotychczasowy plik parseTrips.js.

Style nowych komponentów

W pobranej paczce znajdziesz również pliki .scss nowych komponentów – OrderForm, OrderOption i OrderSummary. Stwórz dla tych komponentów nowe katalogi w folderze src/components/features, a następnie wstaw do nich pliki ze stylami.

Zmiany w magazynie

W pliku store.js musimy wprowadzić kilka zmian, aby uwzględnić nowe elementy stanu początkowego oraz nowy reducer.

W obiekcie initialState dodaj następujący obiekt:

order: {
  trip: null,
  email: '',
  options: {},
},

Następnie zaimportuj plik orderRedux.js jako orderReducer i dodaj go do obiektu reducers pod kluczem order.

import orderReducer from './orderRedux';

// ...

const reducers = {
  filters: filtersReducer,
  order: orderReducer,
};

Po tych zmianach projekt powinien działać tak samo, jak do tej pory – jednak już za chwilę zaczniemy wprowadzać do niego zmiany.

Zadanie: Wyświetlenie ceny zamówienia

Twoim zadaniem jest stworzenie nowych komponentów funkcyjnych – OrderForm i OrderSummary.

Formularz zamówienia

Stwórz komponent OrderForm, który ma na razie zawierać tylko Row, w nim Col z xs={12}, a w nim komponent OrderSummary.

Wstaw OrderForm do komponentu Trip, który znajdziesz w katalogu src/components/views. Możesz dodać go np. po zamknięciu komponentu </DetailsBox>.

<Grid>
  <Row>
    <Col xs={12}>
      <PageTitle text='Trip options' />
      <OrderForm tripCost={cost} />
    </Col>
  </Row>
</Grid>

W tym komponencie na razie to wszystko – przejdźmy do drugiego z nich.

Cena wycieczki

W komponencie OrderSummary wstaw <h2> z klasą styles.component (wziętą z zaimportowanego pliku .scss). Na razie wpisz w nim tylko "Total:" i <strong> zawierający jakąś kwotę, np. $12,345.

Teraz sprawdź, czy na stronie pojedynczej wycieczki wyświetla się nagłówek "Trip options" oraz przykładowa cena, którą wpisaliśmy w OrderSummary.

Jeśli tak, to na razie wszystko działa i możemy przejść do obliczenia ceny wycieczki. Wykorzystamy do tego funkcje z katalogu utilsformatPrice oraz calculateTotal. Druga z nich otrzyma jako argument wynik wywołania tej pierwszej. Jednak calculateTotal będzie potrzebować też od nas bazowej ceny wycieczki oraz opcji zamówienia zapisanych w stanie aplikacji.

Pierwszą informację już przekazaliśmy – jako props tripCost w powyższym fragmencie kodu – z komponentu Trip do OrderForm. W takim razie pozostaje jeszcze przekazanie tego propsa z komponentu OrderForm do OrderSummary, co z pewnością nie będzie już dla Ciebie problemem.

Kontener dla OrderForm

Aby jednak mieć dostęp do opcji zamówienia ze stanu aplikacji, musimy stworzyć kontener dla OrderForm. Stwórz go samodzielnie w pliku OrderFormContainer.js, mapując w nim selektor getOrderOptions z pliku orderRedux.js do propsa options.

Pamiętaj, aby w Trip.js zmienić ścieżkę importowania komponentu OrderForm z pliku OrderForm.js na OrderFormContainer.js!

Następnie w komponencie OrderForm przekaż propsa options do OrderSummary.

Wywołanie funkcji pomocniczych

Ostatnim krokiem będzie wywołanie funkcji pomocniczych w komponencie OrderSummary. Aby wyświetlić obliczoną cenę, zaimportuj funkcje calculateTotal oraz formatPrice z ich plików w katalogu src/utils.

Następnie, w kodzie JSX, wywołaj funkcję formatPrice. Jako jej argument podaj wywołanie funkcji calculateTotal, która ma otrzymać dwa argumenty – koszt wycieczki i opcje ze stanu aplikacji.

W rezultacie powinna wyświetlić się cena produktu. Aby szybko sprawdzić, czy na pewno działa jej obliczanie, możesz w pliku pricing.json, dla opcji o "id": "adults", zmienić wartość "defaultValue" z 1 na 2. Cena wyświetlana w OrderSummary powinna zostać podwojona, ponieważ właśnie zmieniliśmy domyślną liczbę osób biorących udział w wycieczce.

Teraz już widzisz cenę obliczoną na podstawie opcji zapisanych w stanie aplikacji – za chwilę zajmiemy się implementacją komponentów, które pozwolą nam na modyfikację tych opcji!

18.3. Opcje zamówienia

Podliczanie ceny wycieczki z uwzględnieniem opcji mamy już zrobione. Wykorzystaliśmy do tego przygotowaną wcześniej funkcję calculateTotal. Zanim przejdziemy dalej, warto, żebyśmy szybko omówili jej działanie – dzięki temu lepiej zrozumiemy, jak zapisywać wartości wybranych opcji w stanie aplikacji.

Architektura opcji zamówienia

Przygotowując funkcję calculateTotal założyliśmy, że w stanie aplikacji będziemy mieli obiekt order, a w nim obiekt options. W tym ostatnim, kluczami będą id opcji, a wartościami – to, co wybrał użytkownik (lub, na początku działania aplikacji, domyślne wartości z pricing.json).

W przypadku opcji typu dropdown, icons i number nie będzie problemu – będzie jedna wartość dla każdej opcji. Może to być id wybranej wartości lub liczba w przypadku opcji typu number.

Inaczej będzie funkcjonować opcja typu checkboxes, ponieważ można w niej zaznaczyć kilka wartości. Dlatego w stanie aplikacji zapiszemy tablicę, zawierającą id wszystkich wybranych wartości.

Funkcja calculateTotal iteruje przez wszystkie opcje zapisane w stanie aplikacji, dla każdej nich sprawdzając jej ustawienia w pricing.json. Następnie rozpatrywanych jest kilka warunków w bloku if...else if:

  • jeśli wartość opcji w stanie aplikacji jest tablicą, a w pricing.json jest tablica values dla tej opcji, to bierzemy pod uwagę cenę każdego z zaznaczonych wariantów – ta sytuacja będzie dotyczyła opcji typu checkboxes,
  • jeśli wartość opcji w stanie aplikacji nie jest tablicą, ale w pricing.json jest tablica values dla tej opcji, to znajdujemy ten element z values, który ma id zapisane jako wartość tej opcji w stanie aplikacji (czyli robimy to samo, co powyżej, tyle że bez pętli, bo jest tylko jeden możliwy wybór), i wtedy bierzemy pod uwagę cenę tego wariantu opcji,
  • wreszcie, jeśli mamy do czynienia z opcją typu number, to mnożymy jej wartość przez cenę zawartą w pricing.json dla tej opcji.

Plan komponentów opcji zamówienia

Za chwilę stworzymy komponent OrderOption. Będziemy wykorzystywać go w OrderForm do wyświetlenia pojedynczej opcji. Jednak sam OrderOption nie będzie renderował wyłącznie diva wrappującego opcję oraz nazwę opcji. Do wyświetlenia kontrolki formularza – np. <select> – będzie korzystał z osobnego komponentu.

Potrzebujemy więc komponentu OrderOption oraz po jednym komponencie dla każdego z typów opcji – dropdown, icons, checkboxes i number. Zakładamy jednak, że te subkomponenty będą wykorzystywane tylko i wyłącznie w komponencie OrderOption. Dlatego wprowadzimy nową odmianę struktury plików w naszym projekcie – wszystkie te komponenty cząstkowe będą znajdować się w katalogu src/components/features/OrderOption, razem z komponentem OrderOption.

W ten sposób nieco zmieniamy definicję katalogu OrderOption – nie będzie katalogiem jednego komponentu, ale katalogiem pojedynczej funkcjonalności, na którą może składać się kilka komponentów.

Możesz też pomyśleć o tym inaczej: moglibyśmy pozostać przy jednym komponencie OrderOption. Zawierałby on składnię switch, która – w zależności od typu opcji – zwracałaby np. <select> albo zestaw checkboksów, etc. To by jednak oznaczało, że ten komponent miałby dość skomplikowany i rozbudowany kod. Dlatego wygodniej nam będzie wyeksportować każdy z wariantów tego komponentu do osobnego pliku.

Zacznijmy jednak od podstaw.

Wyświetlanie pojedynczej opcji

Na początku stwórz komponent funkcyjny OrderOption, renderujący diva z klasą styles.component z zaimportowanych styli, w którym umieścimy <h3> z klasą styles.title zawierający nazwę opcji (np. "Car Rental").

Następnie w kodzie JSX w OrderForm.js, przed Col, w którym znajduje się OrderSummary, wstaw mapowanie tablicy pricing (zaimportowanej z pricing.json). Dla każdej opcji z tej tablicy chcemy renderować Col z md={4}, a w nim <OrderOption>, do którego rozpakujemy obiekt tej opcji. Pamiętaj, że musimy też ustawić key dla Col – może mieć wartość id danej opcji.

W rezultacie na stronie powinny wyświetlić się nazwy każdej z opcji.

Wyświetlanie odpowiedniego typu opcji

Stwórz teraz w katalogu komponentu OrderOption cztery proste komponenty funkcyjne, które na początek mają tylko renderować diva z nazwą komponentu. Ich nazwy to:

  • OrderOptionDropdown,
  • OrderOptionIcons,
  • OrderOptionNumber,
  • OrderOptionCheckboxes.

Następnie zaimportuj je w pliku OrderOption.js. Pod importami, przed kodem tego komponentu, dodaj następujący obiekt:

const optionTypes = {
  dropdown: OrderOptionDropdown,
  icons: OrderOptionIcons,
  checkboxes: OrderOptionCheckboxes,
  number: OrderOptionNumber,
};

Kluczami tego obiektu są typy opcji, a wartościami – komponenty, które im odpowiadają. Dzięki temu możemy zmienić kod JSX komponentu OrderOption na:

const OrderOption = ({name, type, ...otherProps}) => {
  const OptionComponent = optionTypes[type];
  if(!OptionComponent){
    return null;
  } else {
    return (
      <div className={styles.component}>
        <h3 className={styles.title}>{name}</h3>
        <OptionComponent
          {...otherProps}
        />
      </div>
    );
  }
};

Wartością stałej OptionComponent będzie jeden z komponentów z obiektu optionTypes. Wykorzystujemy go w kodzie JSX i przekazujemy mu wszystkie propsy otrzymane przez OrderOption, poza name i type. Gdyby z jakiegokolwiek powodu w pricing.json znalazła się opcja typu, który nie jest obsługiwany przez nasz kod, komponent OrderOption zwróci null, czyli niczego nie będzie renderował na stronie.

Jeśli wszystko poszło dobrze, na stronie pod każdą nazwą opcji wyświetla się teraz nazwa komponentu, odpowiadającego za dany typ opcji.

Propsy w komponencie opcji

Zastanówmy się teraz, jakie propsy będą potrzebne naszym nowym komponentom. W OrderForm już przekazujemy do OrderOption wszystkie właściwości danej opcji, które są zapisane w pricing.json – ale to jest tylko konfiguracja. Brakuje nam jeszcze informacji o aktualnej wartości tej opcji (zapisanej w stanie aplikacji) oraz funkcji pozwalającej na zmianę opcji.

Zacznijmy od aktualnej wartości – w tym celu z OrderForm do OrderOption musimy przekazać props currentValue o wartości options[id], gdzie id musisz zamienić na odpowiednie wyrażenie, którego wartość to id danej opcji (np. car-rental).

Pamiętaj, że wcześniej już ustawiamy w stanie aplikacji domyślne wartości dla każdej z opcji, więc props currentValue nigdy nie powinien być undefined (ale może mieć wartość pustego ciągu znaków, albo pustej tablicy). Możesz łatwo sprawdzić, czy ustawiasz prawidłową wartość, za pomocą narzędzi developerskich (zakładka React) – komponent OrderOption dla opcji adults i children powinien domyślnie mieć wartość liczbową, taką samą, jaka została ustawiona dla tych opcji w pricing.json.

Przechodząc do drugiego propsa, otwórz OrderFormContainer.js i dodaj mapowanie dispatchera akcji setOrderOption, zaimportowanej z orderRedux.js, do propsa o tej samej nazwie. To mapowanie ma przyjmować jeden argument i przekazywać go do kreatora akcji (czyli funkcji setOrderOption).

Możemy teraz wrócić do komponentu OrderForm i przekazać do OrderOption ostatni props, czyli setOrderOption – jest to zarówno nazwa propsa w OrderForm, jak i przekazywanego do OrderOption, więc wpiszemy po prostu setOrderOption={setOrderOption}.

Komponenty typów opcji

Za moment będziemy mogli już wykorzystać te żmudnie przekazywane propsy, ale najpierw zmodyfikujemy jeszcze jeden z nich! Przejdź do OrderOption.js. W destrukturyzacji propsów dodaj id i setOrderOption, a dla wykorzystywanego w kodzie JSX komponentu OptionComponent dodaj propsa:

setOptionValue={value => setOrderOption({[id]: value})}

Jest to funkcja strzałkowa, która wywołuje funkcję setOrderOption, przekazując jej obiekt. W tym obiekcie jest jedna właściwość, której kluczem będzie zawartość zmiennej (a w tym wypadku – propsa) id, a wartością – argument funkcji strzałkowej.

Jak pamiętasz, nasza struktura zapisywania wybranych opcji zakłada, że zapisujemy je w obiekcie, w którym kluczami są id opcji. Właśnie dlatego musimy tutaj użyć takiego formatu. Łatwiej nam jest jednak zrobić to raz w tym komponencie niż później z osobna dla każdego z komponentów dla poszczególnych typów opcji.

To wszystko w tym pliku – nie musimy przekazywać currentValue, ponieważ nie używamy tego propsa, więc znalazł się w otherProps, które w całości przekazujemy do subkomponentu.

Opcja dropdown

Czas na komponent OrderOptionDropdown! Na przykładzie tego komponentu omówimy sobie wymagania, które muszą spełniać wszystkie subkomponenty OrderOption*.

const OrderOptionDropdown = ({values, required, currentValue, setOptionValue}) => (
  <select
    className={styles.dropdown}
    value={currentValue}
    onChange={event => setOptionValue(event.currentTarget.value)}
  >
    {required ? '' : (
      <option key='null' value=''>---</option>
    )}
    {values.map(value => (
      <option key={value.id} value={value.id}>{value.name} ({formatPrice(value.price)})</option>
    ))}
  </select>
);

Znajdziemy w nim jeden element główny (<select>), który ma klasę odpowiadającą temu komponentowi. Pamiętaj, że style tego komponentu nie mają osobnego pliku .scss, tylko znajdują się w OrderOption.scss.

Wartość tego elementu będzie równa propsowi currentValue, a do eventu zmiany wartości (onChange) przypisaliśmy funkcję strzałkową. Ta funkcja przyjmuje event jako argument i zwraca wywołanie funkcji setOptionValue, której argumentem jest wartość elementu.

Następnie mamy blok kodu, który sprawdza, czy props required jest prawdziwy. Jeśli tak, wstawia pusty ciąg znaków, ale jeśli jest fałszywy (albo nie jest ustawiony), to zostanie wyrenderowany <option> z pustą wartością i tekstem ---. Wykorzystujemy ten zabieg, ponieważ jeśli w pricing.json opcja ma ustawione "required": true, to powinny być do dyspozycji tylko wartości zdefiniowane w tym pliku. Jeśli jednak opcja nie jest wymagana, chcemy dodać <option>, który pozwoli na brak wyboru, czyli rezygnację z tej opcji.

Wreszcie, ostatni fragment kodu to mapowanie po wartościach tej opcji. Dla każdej z nich renderujemy <option>, któremu ustawiamy key, value oraz treść. Bardzo ważne, że wartością tego <option> musi być id danej wartości, ponieważ to po nim rozpoznajemy, która opcja jest wybrana.

Zwróć uwagę, że w treści <option> używamy funkcji formatPrice z utils, aby sformatować cenę tej wartości opcji.

Opcja icons

W tym przypadku zamiast elementu <select> zawierającego wiele <option>, będziemy mieli div zawierający wiele divów. Zewnętrzny div będzie miał tylko className, a wewnątrz niego wykonamy mapowanie po values, w którym wyrenderujemy wewnętrzne divy. Powinny one mieć:

  • className równy styles.icon oraz styles.iconActive – ale tylko jeśli dany element powinien być aktywny,
  • klucz key,
  • onClick, w którym funkcja strzałkowa będzie wywoływać funkcję setOptionValue i przekazywać jej id wartości opcji (tej, po której iterujemy),
  • w treści:
    • komponent Icon z nazwą ikony z propsów wartości opcji,
    • nazwę tej wartości opcji,
    • cenę tej wartości opcji, sformatowaną za pomocą funkcji formatPrice z utils.

Przed mapowaniem dodaj analogicznego, wewnętrznego diva, dla pustej wartości – ale tylko jeśli required jest fałszywe lub nieustawione. Będzie się on różnił od tego użytego w mapowaniu tym, że nie będzie miał klucza key, do funkcji setOptionValue będzie przekazywał pusty ciąg znaków, w treści będzie miał ikonę o nazwie times-circle, a zamiast nazwy będzie tekst "none".

Opcja number

Komponent OrderOptionNumber będzie dużo prostszy – dodaj w nim div z klasą styles.number, w nim wykorzystamy HTML-owy input type="number", więc cała logika działania tego komponentu będzie realizowana przez przeglądarkę. Wystarczy więc ustawić poprawne właściwości tego elementu:

  • klasę styles.inputSmall,
  • type='number',
  • value równe propsowi currentValue,
  • min i max równe odpowiadającym im właściwościom propsa limits,
  • onChange identyczny jak w przypadku opcji dropdown.

Obok inputa warto dodać też sformatowaną cenę tej opcji.

Opcja checkboxes

Czas na najbardziej wymagający z tego zestawu subkomponentówOrderOptionCheckboxes. Od strony samego renderowania nie będzie jednak trudny – będzie bardzo podobny do OrderOptionIcons, tyle że:

  • zewnętrzny div będzie miał klasę styles.checkboxes,
  • nie będzie opcjonalnego wewnętrznego diva dla pustej wartości,
  • w mapowaniu wartości opcji, zamiast diva użyjemy <label> z kluczem key, w którym znajdzie się:
    • <input> typu checkbox,
    • nazwa oraz cena tej wartości opcji.

Input będzie miał właściwości:

  • value równe id tej wartości opcji,
  • checked, które będzie prawdziwe, jeśli to id znajduje się w tablicy currentValue,
  • onChange, które wywoła funkcję setOptionValue, przekazując jej odpowiednią tablicę wartości (wyjaśnienie poniżej).

Dlaczego piszemy o tablicy currentValue? Dlatego, że może być zaznaczonych kilka wartości tej opcji! Dlatego w stanie aplikacji musimy zapisywać tablicę, której elementami są id poszczególnych wartości opcji, które zostały zaznaczone.

W tym samym pliku, pomiędzy importami a kodem komponentu, dodaj tę funkcję:

const newValueSet = (currentValue, id, checked) => {
  if(checked){
    return [
      ...currentValue,
      id,
    ];
  } else {
    return currentValue.filter(value => value != id);
  }
};

Za chwilę wykorzystamy tę funkcję do wygenerowania tablicy, którą zapiszemy w stanie aplikacji dla tej opcji. Najpierw jednak przemyślmy, co robi ta funkcja.

Skoro currentValue jest tablicą, to w momencie zaznaczenia lub odznaczenia checkboksa musimy zmienić tę tablicę. Jeśli checkbox został zaznaczony, to wystarczy dodać id tej wartości opcji do tablicy (robimy to w bloku if). W przeciwnym wypadku musimy usunąć to id z tablicy – ale nie możemy jej modyfikować, tylko musimy stworzyć nową tablicę, co robimy za pomocą metody filter w bloku else.

Wobec tego props onChange w inpucie będzie wykorzystywał tę funkcję, przekazując jej niezbędne informacje:

onChange={event => setOptionValue(newValueSet(currentValue, value.id, event.currentTarget.checked))}

Zakładamy tutaj, że mapując values, nazywasz pojedynczy element value.

Poprawa wyglądu opcji zamówienia

Wreszcie, możemy zająć się wyglądem opcji zamówienia. Wystarczą drobne zmiany w komponencie OrderForm.

Podsumowanie opcji zamówienia

W rezultacie powyższych zmian, na stronie powinny już działać wszystkie opcje zamówienia. Użyj zakładki Redux w narzędziach developerskich, aby zbadać, jakie akcje są wysyłane w momencie zmiany którejś z opcji, i jak wpływają one na stan aplikacji.

Zwróć uwagę, że używamy tego samego komponentu na stronach wszystkich wycieczek – dlatego po przejściu na inną wycieczkę, wybrane wartości opcji powinny być zapamiętane. Zmieni się jednak cena wycieczki, ponieważ każda wycieczka ma inną bazową cenę.

Zadanie: Implementacja nowych komponentów

Twoim zadaniem jest dodanie dwóch nowych typów opcji zamówienia. Pierwszym z nich będzie pole tekstowe, a drugim – wybór daty.

Do pliku pricing.json dodaj te trzy obiekty:

{
  "id": "name",
  "name": "Your name",
  "type": "text"
},
{
  "id": "contact",
  "name": "Contact info (phone, email, etc.)",
  "type": "text"
},
{
  "id": "start-date",
  "name": "Preferred trip start",
  "type": "date"
}

Oba nowe typy opcji będą wymagały nowych komponentów – OrderOptionText i OrderOptionDate. Pierwszy z nich będzie po prostu inputem typu text. Do drugiego zastosuj pakiet react-datepicker.

Podpowiedź do react-datepicker

W dokumentacji znajduje się słabo widoczna informacja o tym, że w przypadku używania CSS modules – których używamy w naszym projekcie – należy zastosować inną ścieżkę do pliku CSS:

import 'react-datepicker/dist/react-datepicker-cssmodules.css';

Mamy nadzieję, że ta podpowiedź oszczędzi Ci kilku siwych włosów... ;)

Powodzenia!

18.4. Podsumowanie

Wspólnie udało nam się stworzyć całkiem rozbudowany projekt! W następnym module zajmiemy się dalszym jego rozwojem – dodamy wysyłanie zamówienia do API oraz upewnimy się, że nasza aplikacja działa poprawnie.

Dla chętnych

Jeśli masz ochotę dalej rozwijać ten projekt przed przejściem do kolejnego modułu, mamy dla Ciebie kilka propozycji.

  1. Na stronie Trips dodaj filtrowanie po regionie, do którego jest dana wycieczka.
  2. W OrderSummary dodaj informację o dacie rozpoczęcia i zakończenia wycieczki, w oparciu o wybraną datę startu oraz liczbę dni danej wycieczki.
  3. Zmodyfikuj projekt w taki sposób, aby wszystkie teksty wyświetlane na stronie, które są wpisane w kodzie JSX, były zapisane w osobnym pliku JSON i importowane do odpowiednich plików.

18.5. Quiz powtórkowy

Na koniec tego modułu przygotowaliśmy dla Ciebie quiz powtórkowy. Pomoże Ci on powtórzyć wiedzę z poprzednich modułów.

Odpowiedzi tego quizu nie są nigdzie zapisywane, więc są tylko do Twojej wiadomości. Ten quiz ma Ci posłużyć jako pomoc w nauce – dlatego pod każdym pytaniem znajdziesz guzik, który sprawdzi poprawność Twoich odpowiedzi oraz poda Ci wyjaśnienie zagadnienia poruszanego w tym pytaniu.

1. Wybierz wszystkie poprawnie zapisane funkcje strzałkowe, które zwrócą jakąś liczbę. Załóż, że argumenty (a i/lub b) będą zawsze liczbami.

Wyjaśnienie

To zadanie mogło przyprawić o zawrót głowy – ale do jego rozwiązania nie musisz uczyć się na pamięć składni funkcji strzałkowych. Wystarczy zapamiętać kilka podstawowych zasad.

Podstawowa składnia

  • zwykła funkcja anonimowa ma składnię: function(a){return a+1;},
  • funkcję strzałkową możemy uzyskać, usuwając słowo function oraz dodając strzałkę,
  • w ten sposób osiągniemy składnię: (a) => {return a+1;}.

Lista argumentów

  • użycie nawiasu okalającego argumenty jest zawsze poprawne,
  • jeśli deklarujemy tylko jeden argument, nie musimy pisać nawiasów,
  • czyli poprawne jest: (a) => {return a+1;} oraz a => {return a+1;},
  • ale kiedy nie ma argumentów lub jest ich więcej niż jeden, musimy używać nawiasów,
  • czyli zapiszemy: () => {return 1;} i (a,b) => {return a+b;}.

Zwracana wartość

  • po strzałce możemy używać nawiasów klamrowych, a w nich wyrażenia return,
  • jednak jeśli nie potrzebujemy wykonywać żadnych innych operacji poza zwróceniem wartości, możemy użyć nawiasów okrągłych zamiast klamrowych i pominąć słowo return,
  • czyli (a,b) => {return a+b;} zadziała tak samo, jak (a,b) => (a+b),
  • co więcej, jeśli całe wyrażenie zapisujemy w jednej linii, możemy nawet w ogóle nie stosować nawiasów po strzałce,
  • czyli poprawny jest zapis (a,b) => a+b.

Od powyższych zasad musimy nieco odstąpić np. kiedy destrukturyzujemy obiekt przekazany w argumencie lub zwracamy obiekt. Weźmy na przykład taką funkcję:

const generateFragments = function({name, age, employer}){
  return {
    withAge: name + ' (' + age + ')',
    employee: name + ' working at ' + employer,
    full: name + ' (' + age + ') working at ' + employer,
  };
};

const mentions = generateFragments({
  name: 'John Doe',
  age: 30,
  employer: 'Acme inc.',
});

Możemy ją przekształcić na funkcję strzałkową:

const generateFragments = ({name, age, employer}) => ({
  withAge: name + ' (' + age + ')',
  employee: name + ' working at ' + employer,
  full: name + ' (' + age + ') working at ' + employer,
});

Mimo że ta funkcja przyjmuje tylko jeden argument (który jest obiektem), nie możemy pominąć nawiasów okrągłych, ponieważ używamy destrukturyzacji, przez co zapis byłby niejednoznaczny.

Analogicznie, zwracaną wartością jest obiekt, ale gdybyśmy nie objęli go nawiasami okrągłymi, byłby zinterpretowany jako blok kodu (porównaj z powyższymi przykładami wykorzystującymi słowo return).

2. Wybierz wszystkie poprawne wyrażenia, wykorzystujące szablony. Przez "poprawne" rozumiemy takie, które w stałej output zapiszą tekst z wykorzystaniem zmiennych i/lub innych fragmentów JS wykorzystanych w szablonie.

Załóż, że wcześniej zostały zdefiniowane następujące zmienne:

const userName = 'Ann';
const notifications = [
  'You have a new message!',
  'Bob has birthday tomorrow',
  'Buy this thing!',
];

Wyjaśnienie

To zadanie było nieco podchwytliwe i wymagało dobrego wzroku. Specjalnie jednak w pierwszych trzech odpowiedziach użyliśmy "normalnych" pojedynczych cudzysłowów, zamiast backticków. Taka pomyłka może Ci się często zdarzać i warto pamiętać o sprawdzeniu, czy użyliśmy poprawnego znaku.

Szablony wykorzystują backtick, czyli `. Jeśli w szablonie chcemy wykorzystać jakiś fragment JS – np. zmienną – możemy użyć wyrażenia ${}. Wewnątrz tych nawiasów klamrowych możemy wpisać dowolne wyrażenie JS, np. jeśli nie chce nam się liczyć, możemy napisać `Użyj ${1000/5} gram mąki.`. Oczywiście, przeważnie będziemy używać szablonów, wykorzystując zmienne, tak jak pokazaliśmy w powyższych (poprawnych) odpowiedziach na to pytanie.

3. Załóżmy, że w tablicy colors mamy zapisane nazwy kolorów:

const colors = [
  'red',
  'green',
  'blue',
  'white',
  'black',
  'purple',
];

Chcemy zapisać te nazwy w osobnych zmiennych, tak aby pierwsze trzy były zapisane w zmiennych/stałych o nazwach: first, second i third, a pozostałe kolory mają zostać zapisane w tablicy others. Wybierz te wyrażenia, które pozwolą nam osiągnąć ten rezultat.

Wyjaśnienie

Destrukturyzacja jest nieco skomplikowanym zagadnieniem i na początku może od Ciebie wymagać większego skupienia. Postaraj się jednak zapamiętać, że:

  • destrukturyzacja jest deklaracją kilku zmiennych jednocześnie, więc tak samo, jak przy zwykłej deklaracji, używamy const lub let,
  • jeśli destrukturyzujemy tablicę, używamy [], a w przypadku obiektu – {},
  • destrukturyzacja nie zmienia w żaden sposób źródła danych (w naszym przypadku: tablicy colors),
  • możesz pominąć elementy, których nie potrzebujesz zapisać w zmiennej,
  • jeśli chcesz z tablicy wyciągnąć tylko niektóre elementy, możesz pominąć niepotrzebne używając przecinków,
  • zapisanie wszystkich pozostałych elementów w jednej zmiennej realizujemy, dodając ... przed nazwą stałej/zmiennej.

Wiedząc to, spójrz ponownie na powyższe odpowiedzi. Od razu możesz odrzucić wszystkie, które nie zaczynają się od const lub let. Następnie, skoro wiemy, że chcemy pozostałe kolory zapisać w tablicy, to potrzebne będzie wyrażenie ...others.

Po tej eliminacji zostało już tylko parę możliwości, z których błędną jest tylko jedna – ta zawierająca [...others]. Jest ona błędna, ponieważ do tablicy others zapisuje wszystkie elementy z colors.

Poeksperymentuj samodzielnie z tymi wyrażeniami, aby lepiej je zrozumieć.

;